/***************************************************
 * VIPER.java
 * -----------------------
 * Revision: v.2.0
 * Author: Ji Kim
 * Date Created: 12/05/2009
 * Description:
 *   Preliminary sender/receiver program that captures
 *   and digitizes audio from a microphone to create
 *   an audio data stream to be used for diversity
 *   coding and transmission.
 *   Also contains debug code for default path VoIP
 *   testing and diagnostics.
 * Last Back-Up: 01/24/10
 * Revision History: *Cleaned up and updated comments
 * 					 *Added socket-level access debug
 * 					 *Simple diversification capability
 * 					 *Improved Sender/Receiver code
 * 					 *Compatibility with custom stream/DataSource
 * 
 * Based on MediaTransmitter.java demo source code
 * in JMF package. This can be found in the link
 * provided in the tutorial at:
 * http://carfield.com.hk/document/java/tutorial
 *    /jmf_tutorial.pdf
 * -----------------------  
 ***************************************************/

import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.*;
import javax.media.control.*;
import javax.media.protocol.*;
import javax.media.format.*;

import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Vector;

public class VIPER {

	public MediaLocator mediaLocator = null;
	public DataSink dataSink = null;
	public Processor mediaProcessor = null;

	private DataSource digitizedAudioSource;

	//Define audio codec type
	private static final Format[] FORMATS = new Format[]{
		new AudioFormat(AudioFormat.MPEG,
				44100,
				16,
				2,
				AudioFormat.BIG_ENDIAN,
				AudioFormat.SIGNED,
				8,
				8000.0,
				Format.byteArray)
	};
	//Define output of Processor
	private static final ContentDescriptor CONTENT_DESCRIPTOR =
		new ContentDescriptor(ContentDescriptor.RAW); //we just want raw data buffers

	//Constructor, define audio capture device as DataSource
	public VIPER(DataSource ds) {
		try{
			setDataSource(ds);
		}catch(CannotRealizeException cre){
			System.err.println("CannotRealizeException during construction.");
		}catch(NoProcessorException npe){
			System.err.println("NoProcessorException during construction.");
		}catch(IOException ioe){
			System.err.println("IOException during construction.");
		}
	}

	public void startEncoding() throws IOException {
		mediaProcessor.start(); //Start the Processor: encode audio data by defined codec
	}

	public void setDataSource(DataSource ds) throws IOException,
	NoProcessorException, CannotRealizeException {
		
		/*
		Control c = (Control)ds.getControl("javax.media.control.BufferControl");
		if (c!=null){
			System.out.println("DEBUG: ds has BufferControl...");
			((BufferControl)c).setBufferLength(16);
		}
		*/
		
		//Construct Processor that will handle encoding with specified codec.
		mediaProcessor = Manager.createRealizedProcessor(
				new ProcessorModel(ds, FORMATS, CONTENT_DESCRIPTOR));
		
		
		Control [] test = mediaProcessor.getControls();
		/*
		BitRateControl frcon = null;
		for(int i=0;i<test.length;i++){
			System.out.println(test[i]);
			if(test[i] instanceof BitRateControl){
				System.out.println("Ji: "+i);
				frcon = (BitRateControl)test[i];
			}
		}
		frcon = (BitRateControl)test[4];
		frcon.setBitRate(64000);
		*/
		
		//Isolate the BufferControl controller from Processor and manually set buffer times.
		BufferControl bcon = (BufferControl)test[1];
		bcon.setBufferLength(16); //in ms
		
		System.out.println("::DEBUG, Buffer Length: "+bcon.getBufferLength());
		
		// Get the digitized and encoded data stream as a DataSource
		digitizedAudioSource = mediaProcessor.getDataOutput();
	}

	//Pass digitized audio stream to Extractor to allow socket-level access.
	public LiveStream extract(InetAddress ip, int port) throws IOException{
		//Data transmission takes place within Extractor.
		Extractor ex = new Extractor(digitizedAudioSource, ip, port);
		ex.setDropRate(0.3);
		ex.start();
		return ex.getLiveStream();
	}

	public DataSource getSource(){
		return digitizedAudioSource;
	}

	public static void main(String [] args) {

		DataSource source = null;
		PushBufferStream pbs = null;
		Player player = null;
		LiveStream ls;

		DatagramSocket rxsock = null;

		int maxPacketSize = 9000; // For receiver code, arbitrary
		InetAddress destIP = null;
		int destPort;
		// Resolve IP Address
		try{
			destIP = InetAddress.getByName(args[0]);
		}catch(Exception uhe){
			System.err.println("Unknown Host Name.");
		}
		destPort = Integer.valueOf(args[1]);

		/** SENDER CODE **/
		try {
			//Set type of audio format expected from capture device
			AudioFormat format = new AudioFormat(AudioFormat.LINEAR, //encoding type
					44100, //sampling frequency 
					16, //sample depth
					2); //channels

			// Get a list of connected capture devices and set the first available one
			// as the designated device
			Vector devices = CaptureDeviceManager.getDeviceList(format);
			System.out.println("::Retrieving list of available capture devices...");
			CaptureDeviceInfo cdi = (CaptureDeviceInfo) devices.elementAt(0);
			//System.out.println("::Capture device set with the following properties: ");
			//System.out.println(cdi);

			//Create DataSource from capture device
			source = Manager.createDataSource(cdi.getLocator());
			System.out.println("::Capture Device connected!");

			//Construct VIPER object and assign capture device DataSource to it.
			VIPER transmitter = new VIPER(source);
			System.out.println("::DataSource Initialized!");

			//Begin encoding audio stream.
			transmitter.startEncoding();
			transmitter.getSource().start();
			System.out.println("::Audio Encoding Started...");

			//Extract data stream from DataSource object
			//Extractor also doubles as the transmitter that sends each individual packetized data.
			ls = transmitter.extract(destIP, destPort);
			System.out.println("::Transmitting to " + destIP + " on port " + destPort +"...");

			/** DEBUG CODE **/
			////////////////////////////////////////////////////////////////////////////////////////////
			// Below for testing audio streams locally without socket-level manipulation.
			//Grab all available stream from capture device as PushBufferStreams
			//PushBufferStream [] streams = ((PushBufferDataSource)transmitter.getSource()).getStreams();
			//
			//Construct custom DataSource from extracted data stream
			//PBDataSource pbds = new PBDataSource(streams[0]);
			//
			//Enable DataSource
			//pbds.connect();
			//
			//Construct player for local playback and start it
			//player = Manager.createRealizedPlayer(pbds);
			//player.start();
			//System.out.println("::DEBUG, Locally Playback Audio...");
			////////////////////////////////////////////////////////////////////////////////////////////

			////////////////////////////////////////////////////////////////////////////////////////////
			// Below for testing custom PushBufferStream with socket-level access for local playback.
			//Construct DataSource from custom stream.
			//CustomDataSource cds = new CustomDataSource(ls);
			//
			//Enable DataSource
			//cds.connect();
			//
			//Construct player for local playback and start it
			//player = Manager.createRealizedPlayer(cds);
			//player.start();
			//System.out.println("::DEBUG, Locally Playback Audio...");
			////////////////////////////////////////////////////////////////////////////////////////////

			/** RECEIVER CODE **/
			// Uncomment below to initialize VIPER as receiver.
			// Need to integrate this so you can choose sender or receiver.

			// Construct a new socket on specified port.
			// All packets received on this port will be assumed to be VIPER packets.
			/* UNCOMMENT ME
			try{
				rxsock = new DatagramSocket(destPort);
				System.out.println("Initializing socket.");
			}catch(Exception e){
				System.out.println("Failed to open socket.");
			}

			// Construct the custom PushBufferStream
			LiveStream rxls = new LiveStream();
			// Initialize receive stream with DataSource
			CustomDataSource rxcds = new CustomDataSource(rxls);
			rxcds.connect(); 

			// Construct and start the audio player
			player = Manager.createRealizedPlayer(rxcds);
			player.start();
			System.out.println("Starting player...");

			// The code below tests de-diversification for (2,3,2) scheme with all type 2 packets lost.
			byte[] buff = new byte[maxPacketSize]; //Storage buffer for incoming packets
			byte[] buff1 = null; //Storage for VHeader = 1 type packets
			byte[] buff3; //Storage for VHeader = 3 type packets
			byte[] buffrecovered=null; //also VHeader = 2

			// Continually receive packets...
			// Replace this with Threads later...
			while(true){
				// Prepare storage for incoming UDP packet, this saves payload into buff.
				DatagramPacket p = new DatagramPacket(buff, maxPacketSize);
				// Try receiving packet on the socket.
				try{
					rxsock.receive(p);
					//System.out.println("Reading...");
				}catch(IOException ioe){
					System.out.println("IOException during receiving packets.");
				}

				/** DE-DIVERSIFICATION LOGIC **/
			// If packet is type 1, save payload into buff1 and update the stream since this packet wasn't encoded.
			/* UNCOMMENT ME
				if(buff[0]==1){ //Examine VIPER header...
					buff1= new byte[buff.length-1];
					System.arraycopy(buff,1,buff1,0,buff.length-1);
					if(p!=null){
						//System.out.println("Packet Received!!");
						rxls.update(buff1);

					}
				}
				// If packet is type 3, then decode using type 1 packet and update stream with recovered type 2 packet.
				else if(buff[0]==3){
					buff3= new byte[buff.length-1];
					buffrecovered= new byte[buff.length-1];
					System.arraycopy(buff,1,buff3,0,buff.length-1);
					//Perform the decoding with XOR...
					 for (int i =0;i<buff3.length;i++){
					     buffrecovered[i]=(byte)(buff1[i]^buff3[i]);
					     System.out.print(buffrecovered[i]);
					    }
					 if(p!=null){
							//System.out.println("Packet Received!!");
							rxls.update(buffrecovered);
						}
				}				
			}
		UNCOMMENT ME*/
		} catch (Throwable t) {
			t.printStackTrace();
		}
	}

}